import canvas.abstractcanvas;
import canvas.floatcanvas;
import canvas.floatcanvas3d;
import canvas.keysyms;
import canvas.sdlcanvas;
import canvas.sdlkeys;
import console;
import derelict.devil.il;
import derelict.sdl.sdl;
import material.bitmaptexture;
import material.mipmap;
import material.material;
import material.texture;
import material.voltexture;
import material.ashimaterial;
import material.gentextures;
import objects.abstractobject;
import objects.beziermesh;
import objects.trianglemesh;
import objects.objectinstance;
import render.abstractsampler;
import render.filtersampler;
import render.imagerender;
import render.renderer_a2;
import render.renderer_ray;
import render.rendertocanvas;
import render.sampler;
import scene.camera;
import scene.scene;
import shader.lightsources;
import shader.abstractshader;
import shader.litshaderlighting;
import shader.litshaderambientocclusion;
import shader.litshadersphharm;
import shader.litshadersun;
import shader.environment;
import std.boxer;
import std.file;
import std.math;
import std.stdio;
import std.stream;
import std.string;
import cmath = std.c.math;
import utils.color;
import utils.matrix;
import utils.time;
import utils.vector2;
import utils.vector3;

enum AppMode {
    amCmd, amGUI
}

class Application {
    private SDLCanvas mScreenCanvas;
    private Vector2i mRes;
    private Console mConsole;
    private CommandProcessor mCmdProc;
    private RenderToCanvas mRenderCanvas;
    private Renderer_A2 mRendA2;
    private Renderer_Ray mRendRay;
    private ImageRenderer mImageRenderer;
    private FilterSampler mFilter;
    private Camera mCamera;
    private Scene mScene;
    private AbstractShader[char[]] mLitShaders;
    private AppMode mAppMode = AppMode.amGUI;
    private char[] mScriptFilename;
    private AntialiasSampler mAntialias;
    private Vector3f mBallCam;
    private char[] mLastSceneLoad, mLastImageLoad, mLastLightLoad;
    private char[] mLastTextureLoad, mLastVolTextureLoad, mLastEnvMapLoad;
    private int mSubdivCount = 2;
    private bool mMouseCam = false;
    private Time mLastCamTime, mLowResDelta;
    private int mLowPixelRes = 3;
    private bool mAbortCurrentFrame = false;
    private bool mAllowLowResEvents = false;
    private Environment mEnvironment;
    private Time mT0;
    private float mSunTime = 9;  //time of day
    private float mDayLength = 10;  //seconds
    private float mSunDate = 30;
    private bool mSunAnim = false;

    private AbstractObject[char[]] mObjects;
    private char[] mCurrentObj;

    private Material[char[]] mMaterials;
    private char[] mCurrentMat;

    private float mScale = 1.0f;
    private Vector3f mTranslate, mRotate;

    private Vector2i mMouseDownPos, mMouseUpPos;

    private struct BindItem {
        SDLKey sym;
        SDLMod mod;
    }

    private char[][BindItem] mKeyMap;

    private char[] getKeyAction(in SDL_KeyboardEvent ev) {
        BindItem b;
        b.sym = ev.keysym.sym;
        b.mod = ev.keysym.mod & (KMOD_ALT | KMOD_CTRL);
        if (b in mKeyMap)
            return mKeyMap[b];
        else
            return null;
    }

    private bool handleEvent(SDL_Event ev) {
        const float cRotDelta = 0.5f;
        const float cMovDelta = 100.0f;

        float rotDelta = cRotDelta;
        float movDelta = cMovDelta;

        //hack: shift makes you slower
        if ((SDL_GetModState() & KMOD_SHIFT) != 0) {
            rotDelta /= 5.0f;
            movDelta /= 5.0f;
        }

        switch (ev.type) {
            case SDL_MOUSEBUTTONDOWN:
                if (!mMouseCam)
                    mMouseDownPos = Vector2i(ev.button.x, ev.button.y);
                break;
            case SDL_MOUSEBUTTONUP:
                if (!mMouseCam) {
                    mMouseUpPos = Vector2i(ev.button.x, ev.button.y);
                    if (SDL_GetModState() & KMOD_CTRL)
                      //zoom only when modifier pressed
                      setZoom(mMouseDownPos, mMouseUpPos);
                }
                break;
            case SDL_MOUSEMOTION:
                float dx = cast(float)ev.motion.xrel/mRes.x;
                float dy = cast(float)ev.motion.yrel/mRes.y;
                if (mMouseCam) {
                    ubyte mb = SDL_GetMouseState(null, null);
                    if (mb & SDL_BUTTON_LMASK && mb & SDL_BUTTON_RMASK) {
                        //both mouse buttons
                        mCamera.moveRelative(Vector3f(movDelta*dx, -movDelta*dy, 0));
                    } else if (mb & SDL_BUTTON_LMASK) {
                        //left button
                        mCamera.moveRelative(Vector3f(0.0f, 0.0f, -movDelta*dy));
                        mCamera.rotateRelative(Vector3f(0.0f,PI/2*dx,0.0f));
                    } else if (mb & SDL_BUTTON_RMASK) {
                        //right button
                        mCamera.rotateRelative(Vector3f(PI/2*dy, 0.0f, -PI/2*dx));
                    } else {
                        //no button
                    }
                }
                break;
            case SDL_KEYDOWN:
                char[] action = getKeyAction(ev.key);
                if (ev.key.keysym.sym == SDLK_ESCAPE && mMouseCam) {
                    cmdMouseToggle([]);
                    return true;
                }
                switch (action) {
                    case "scrreset":
                        mRenderCanvas.resetScreenRegion();
                        break;
                    case "cam_fwd":
                        mCamera.moveRelative(Vector3f(0, 0, movDelta));
                        break;
                    case "cam_bwd":
                        mCamera.moveRelative(Vector3f(0, 0, -movDelta));
                        break;
                    case "cam_right":
                        mCamera.moveRelative(Vector3f(movDelta, 0, 0));
                        break;
                    case "cam_left":
                        mCamera.moveRelative(Vector3f(-movDelta, 0, 0));
                        break;
                    case "cam_up":
                        mCamera.moveRelative(Vector3f(0, -movDelta, 0));
                        break;
                    case "cam_down":
                        mCamera.moveRelative(Vector3f(0, movDelta, 0));
                        break;
                    case "cam_rot_left":
                        mCamera.rotateRelative(Vector3f(0, -rotDelta, 0));
                        break;
                    case "cam_rot_right":
                        mCamera.rotateRelative(Vector3f(0, rotDelta, 0));
                        break;
                    case "cam_rot_up":
                        mCamera.rotateRelative(Vector3f(rotDelta, 0, 0));
                        break;
                    case "cam_rot_down":
                        mCamera.rotateRelative(Vector3f(-rotDelta, 0, 0));
                        break;
                    case "cam_rot_roll_l":
                        mCamera.rotateRelative(Vector3f(0, 0, rotDelta));
                        break;
                    case "cam_rot_roll_r":
                        mCamera.rotateRelative(Vector3f(0, 0, -rotDelta));
                        break;

                    //ballcam
                    case "ballcam_left":
                        updateBallCam(Vector3f(-rotDelta, 0, 0));
                        break;
                    case "ballcam_right":
                        updateBallCam(Vector3f(rotDelta, 0, 0));
                        break;
                    case "ballcam_up":
                        updateBallCam(Vector3f(0, -rotDelta, 0));
                        break;
                    case "ballcam_down":
                        updateBallCam(Vector3f(0, rotDelta, 0));
                        break;
                    case "ballcam_fwd":
                        updateBallCam(Vector3f(0, 0, movDelta));
                        break;
                    case "ballcam_bck":
                        updateBallCam(Vector3f(0, 0, -movDelta));
                        break;

                    case "mousecam":
                        cmdMouseToggle([]);
                        break;
                    default:
                        break;
                }
                break;
            default:
        }
        return false;
    }

    private void updateBallCam(Vector3f rot) {
        mBallCam += rot;
        float r = mBallCam.x3;
        float phi = mBallCam.x1;
        float rho = mBallCam.x2;
        Vector3f from = Vector3f(r*cos(rho)*cos(phi), r*cos(rho)*sin(phi), r*sin(phi));
        mCamera.lookAtFrom(from + mScene.middle, mScene.middle);
    }

    private void setZoom(Vector2i scrP1, Vector2i scrP2) {
        Vector2f p1 = Vector2f.fromInt(scrP1);
        Vector2f p2 = Vector2f.fromInt(scrP2);
        Vector2f size = Vector2f.fromInt(mScreenCanvas.size);
        mRenderCanvas.setScreenRegion(p1/size, p2/size);
    }

    //camera delegate called every time cam changes
    private void cameraChangeView() {
        mRenderCanvas.pixelRes = mLowPixelRes;
        mLastCamTime = timeCurrentTime();
        mAbortCurrentFrame = true;
    }

    //this is not really called every tick, rather about every rendered line
    private void tick() {
        if (timeCurrentTime() - mLastCamTime >= mLowResDelta && mRenderCanvas.pixelRes > 1) {
            mRenderCanvas.pixelRes = 1;
        }
    }

    //called after every rendered frame
    private void frame() {
        Time t = timeCurrentTime();
        Time deltaT = t-mT0;
        if (mSunAnim) {
            (cast(LitShaderSun)mLitShaders["sun"]).dirByTime(mSunDate,mSunTime);
            mSunTime = cmath.fmodf(mSunTime + cast(float)deltaT.msecs * 24.0f/(mDayLength*1000.0f),24.0f);
            //debug writefln("Time: %sh",mSunTime);
        }
        mT0 = t;
    }

    this(char[][] args) {
        DerelictIL.load();
        ilInit();
        if (args.length > 1 && exists(args[1])) {
            mScriptFilename = args[1];
            mAppMode = AppMode.amCmd;
        }
        if (mAppMode == AppMode.amGUI) {
            DerelictSDL.load();
        }
        mRes = Vector2i(104,104);

        //xxx
        mBallCam = Vector3f(0,0,500);
        mLowResDelta = timeMsecs(250);
    }

    ~this() {
        DerelictIL.unload();
        if (mAppMode == AppMode.amGUI) {
            DerelictSDL.unload();
        }
    }

    public int run() {
        initConsole();

        // *** init renderers ***

        mRendA2 = new Renderer_A2();

        mEnvironment = new Environment();
        initShaders();

        mRendRay = new Renderer_Ray();
        mCamera = new Camera();
        mScene = new Scene();
        mRendRay.camera = mCamera;
        mRendRay.scene = mScene;
        mRendRay.environment = mEnvironment;

        mImageRenderer = new ImageRenderer();
        mFilter = new FilterSampler();

        // *** init canvas renderer ***

        mRenderCanvas = new RenderToCanvas();
        mRenderCanvas.renderer = mRendA2;

        mAntialias = new AntialiasSampler();

        if (mAppMode == AppMode.amGUI) {
            // *** start GUI mode ***

            cmdExec(["initgui.rs"]);

            //init output window/canvas
            mScreenCanvas = new SDLCanvas(mRes,false,true);
            mScreenCanvas.addEventConsumer(&handleEvent);
            mScreenCanvas.onFPSUpdate = &updateFPS;

            mRenderCanvas.onProgress = &renderProgress;
            mCamera.onChangeView = &cameraChangeView;

            //enable interactive command line
            mConsole.start();
            mT0 = timeCurrentTime();

            while (!mScreenCanvas.terminated()) {
                mScreenCanvas.processEvents();
                mConsole.checkCommands();
                tick();

                mScreenCanvas.enterRenderMode();
                mRenderCanvas.render(mScreenCanvas);
                mScreenCanvas.leaveRenderMode();

                frame();
            }

            //doesn't work, but this is where it should be
            mConsole.stop();

            //force SDL_Quit() here (before Derelict unloads...)
            delete mScreenCanvas;

        } else {
            // *** start command-line mode ***
            cmdExec([mScriptFilename]);
        }

        return 0;
    }

    private void initConsole() {
        mCmdProc = new CommandProcessor();
        mCmdProc.registerCommand("#",&cmdComment,"Place for rudiculous stuff not worth to notice");
        mCmdProc.registerCommand("quit",&cmdQuit,"Quit program");
        mCmdProc.registerCommand("setfunc",&cmdSetFunc,"Set test function to be rendered");
        mCmdProc.registerCommand("positive",&cmdPositive,"Enable/disable treshold function");
        mCmdProc.registerCommand("setrend",&cmdSetRend,"Switch active renderer");

        mCmdProc.registerCommand("loadtri",&cmdLoadTri,"Load and display triangle scene",["string"],["sceneFile"]);
        mCmdProc.registerCommand("loadbez",&cmdLoadBez,"Load and display bezier patch scene",["string"],["sceneFile"]);

        mCmdProc.registerCommand("curobj",&cmdCurObj,"Set active object",["string"],["objId"]);
        mCmdProc.registerCommand("objtri",&cmdObjTri,"New object from raw triangle file",["string","string"],["objId","sceneFile"]);
        mCmdProc.registerCommand("objbez",&cmdObjBez,"New object from bezier mesh file",["string","string"],["objId","sceneFile"]);
        mCmdProc.registerCommand("clear",&cmdClear,"Clear scene (will not clear loaded objects)",[],[]);
        mCmdProc.registerCommand("insert",&cmdInsert,"Add object to scene",["string"],["objId"]);

        mCmdProc.registerCommand("newmat",&cmdNewMat,"Create new material and assign to matId",["string","string"],["matType","matId"]);
        mCmdProc.registerCommand("curmat",&cmdCurMat,"Set active material",["string"],["matId"]);
        mCmdProc.registerCommand("mattex",&cmdMatTex,"Load texture into current material to map it on the geometry",["int","string"],["index","filename"]);
        mCmdProc.registerCommand("matcolor",&cmdMatColor,"Set color texture for current material",["int","color"],["index","color"]);
        mCmdProc.registerCommand("matgentex",&cmdMatGenTex,"Generate texture for current material",["int","int","int","string","text"],["index","width","height","textureName","texArgs"],4);
        mCmdProc.registerCommand("matgenvtex",&cmdMatGenVTex,"Generate volume texture for current material",["int","int","int","int","string","text"],["index","width","height","depth","textureName","texArgs"]);
        mCmdProc.registerCommand("assignmat",&cmdAssignMat,"Assign material to current object (no index for global)",["string","int"],["materialId","objMatIdx"],1);

        mCmdProc.registerCommand("savepng",&cmdSavePNG,"Save current view as PNG");
        mCmdProc.registerCommand("loadimg",&cmdLoadImage,"Load and show image");
        mCmdProc.registerCommand("setfilt",&cmdFilter,"Set a sample filter");
        mCmdProc.registerCommand("setsize",&cmdSetSize,"Resize window");
        mCmdProc.registerCommand("aa",&cmdAa,"Set antialiasing filter");
        mCmdProc.registerCommand("aas",&cmdAas,"Set antialiasing sample count (lin/sq)");
        mCmdProc.registerCommand("cmm",&cmdCmm,"Set color mapping mode");
        mCmdProc.registerCommand("exec",&cmdExec,"Execute script");
        mCmdProc.registerCommand("bind",&cmdBind,"Bind key to action");
        mCmdProc.registerCommand("unbind",&cmdUnBind,"Remove an action->key mapping");
        mCmdProc.registerCommand("unbindall",&cmdUnbindAll,"Remove all key bindings");
        mCmdProc.registerCommand("setcam",&cmdSetCam,"Set camera position");
        mCmdProc.registerCommand("resetcam",&cmdResetCam,"Reset camera to show the whole scene",[],[]);
        mCmdProc.registerCommand("saveview",&cmdSaveView,"Save current view to script file");
        mCmdProc.registerCommand("subdiv",&cmdSubdiv,"Set subdivision count");
        mCmdProc.registerCommand("mousecam",&cmdMouseToggle,"Toggle mouse camera");
        mCmdProc.registerCommand("shlit",&cmdShLit,"Set lighting shading mode");
        mCmdProc.registerCommand("setres",&cmdSetRes,"Set low resolution");
        mCmdProc.registerCommand("normip",&cmdNormIp,"Set bezier normal interpolation");
        mCmdProc.registerCommand("loadlit",&cmdLoadLit,"Load light sources");
        mCmdProc.registerCommand("lset",&cmdLSet,"Set light color");
        mCmdProc.registerCommand("cd",&cmdChDir,"Change current directory");
        mCmdProc.registerCommand("chdir",&cmdChDir,"Change current directory");
        mCmdProc.registerCommand("bumpmap",&cmdBumpMap,"Bump mapping");
        mCmdProc.registerCommand("loadenvmap",&cmdLoadEnvMap,"Load environment map");

        mCmdProc.registerCommand("setambientlight",&cmdSetAmbientLight,"Set ambient light color (or off)",["color"],["color"]);
        mCmdProc.registerCommand("setrecdepth",&cmdSetRecDepth,"Path tracing recursion depth",["int"],["depth"]);
        mCmdProc.registerCommand("setcamlight",&cmdSetCamLight,"Set cheap camera light",["bool"],["state"]);
        mCmdProc.registerCommand("simplereflect",&cmdSimpleReflect,"Set simple environment map lookup",["bool"],["state"]);
        mCmdProc.registerCommand("texfilt",&cmdTexFilt,"Set texture filter for texture index of current material",["int","int"],["index","filterId"]);
        mCmdProc.registerCommand("sunanim",&cmdSunAnim,"Toggle sun animation",["bool"],["state"]);
        mCmdProc.registerCommand("settime",&cmdSetTime,"Set current time of day",["float"],["hour"]);

        mCmdProc.registerCommand("scale",&cmdScale,"Set scale factor for next insert",["float"],["scaleFact"]);
        mCmdProc.registerCommand("rotate",&cmdRotate,"Set rotation vector for next insert (degrees)",["float","float","float"],["rotX","rotY","rotZ"]);
        mCmdProc.registerCommand("translate",&cmdTranslate,"Set translation for next insert",["float","float","float"],["transX","transY","transZ"]);
        mCmdProc.registerCommand("loadidentity",&cmdLoadIdentity,"Reset translation/rotation/scaling",[],[]);

        mConsole = new Console(mCmdProc);
    }

    private void initShaders() {
        mLitShaders = null;

        mLitShaders["none"] = null;
        //mLitShaders["normal"] = new LitShaderNormal();
        mLitShaders["light"] = new LitShaderLighting();

        LitShaderAmbientOcclusion skysh = new LitShaderAmbientOcclusion();
        skysh.environment = mEnvironment;
        skysh.skyhack = true;
        mLitShaders["sky"] = skysh;

        LitShaderAmbientOcclusion ambOcc = new LitShaderAmbientOcclusion();
        ambOcc.environment = mEnvironment;
        mLitShaders["ambient"] = ambOcc;

        LitShaderSphHarmonics spsh = new LitShaderSphHarmonics();
        spsh.environment = mEnvironment;
        mLitShaders["sphharm"] = spsh;

        LitShaderSun sunsh = new LitShaderSun();
        sunsh.dirByTime(mSunDate,mSunTime);
        mLitShaders["sun"] = sunsh;
    }

    private void setSize(Vector2i size) {
        mRes = size;
        if (mScreenCanvas)
            mScreenCanvas.resizeCanvas(size);
    }

    private void cmdComment(char[][] args) {
        //ignore
    }

    private void cmdQuit(char[][] args) {
        mScreenCanvas.terminate();
    }

    private void cmdMouseToggle(char[][] args) {
        mMouseCam = !mMouseCam;
        if (mScreenCanvas)
            mScreenCanvas.captureMouse(mMouseCam);
    }

    private void cmdFilter(char[][] args) {
        if (args.length >= 1 && isNumeric(args[0])) {
            //try {
                int size = 0;
                if (args.length > 1 && isNumeric(args[1]))
                    size = atoi(args[1]);
                DefaultFilter filterId = cast(DefaultFilter)atoi(args[0]);
                mFilter.setDefaultFilter(filterId, size);
                //mFilter.renderer = mRenderCanvas.renderer;
                //mRenderCanvas.renderer = mFilter;
            //} catch (Exception e) {
            //    writefln("Error: Invalid filter id");
            //}
        } else {
            writefln("Syntax: setfilt <filterId>");
        }
    }

    private void cmdScale(Box[] args) {
        mScale = unbox!(float)(args[0]);
    }

    private void cmdTranslate(Box[] args) {
        mTranslate = Vector3f(unbox!(float)(args[0]),unbox!(float)(args[1]),unbox!(float)(args[2]));
    }

    private void cmdRotate(Box[] args) {
        float toRad(float degree) {
            return degree*PI/180.0f;
        }

        mRotate = Vector3f(toRad(unbox!(float)(args[0])),toRad(unbox!(float)(args[1])),toRad(unbox!(float)(args[2])));
    }

    private void cmdLoadIdentity(Box[] args) {
        mRotate = Vector3f(0,0,0);
        mTranslate = Vector3f(0,0,0);
        mScale = 1.0f;
    }

    private void cmdSetCamLight(Box[] args) {
        mRendRay.cameralight = unbox!(bool)(args[0]);
    }

    private void cmdSetAmbientLight(Box[] args) {
        RGBColor col = unbox!(RGBColor)(args[0]);
        mRendRay.ambientlight = col;
    }

    private void cmdSetRecDepth(Box[] args) {
        mRendRay.recursiondepth = unbox!(int)(args[0]);
    }

    private void cmdShLit(char[][] args) {
        if (args.length < 1 || !(args[0] in mLitShaders)) {
            writefln("Syntax: shlit <lightingShaderName>");
            writefln("Available lighting shaders:");
            foreach (char[] key, AbstractShader sh; mLitShaders) {
                if (sh)
                    writefln("  %s: %s",key,sh);
                else
                    writefln("  %s: No lighting",key);
            }
            return;
        }

        mRendRay.lightingShader = mLitShaders[args[0]];
        if (mLitShaders[args[0]]) {
            writefln("Lighting shader: %s",mLitShaders[args[0]]);
            mRendRay.cameralight = false;
        } else {
            writefln("Lighting shader: No lighting");
            mRendRay.cameralight = true;
        }
    }

    private void cmdSetRes(char[][] args) {
        if (args.length == 0 || !isNumeric(args[0])) {
            writefln("Syntax: setres <pixelsize>");
            return;
        }
        //mRenderCanvas.pixelRes = atoi(args[0]);
        mLowPixelRes = atoi(args[0]);
    }

    private void cmdAa(char[][] args) {
        if (args.length >= 1 && isNumeric(args[0])) {
            mRenderCanvas.antialias = mAntialias;
            mAntialias.antialiasMethod = cast(AntialiasMethod)atoi(args[0]);
        } else if (args.length >= 1 && args[0] == "none") {
            mRenderCanvas.antialias = null;
            writefln("No antialiasing");
        } else {
            writefln("Syntax: aa <antialiasId>");
            mAntialias.listModes();
        }
    }

    private void cmdAas(char[][] args) {
        if (args.length != 2) {
            writefln("Syntax: aas lin|sq <sampleCount>");
            return;
        }

        if (args[0] == "lin") {
            mAntialias.linearSampleCount = atoi(args[1]);
        } else if (args[0] == "sq") {
            mAntialias.squareSampleCount = atoi(args[1]);
        } else {
            writefln("Invalid sample kind. Valid values: lin|sq");
        }
    }

    private void cmdCmm(char[][] args) {
        if (args.length >= 1 && isNumeric(args[0])) {
            mRenderCanvas.colorMappingMode = cast(ColorMappingMode)atoi(args[0]);
        } else {
            writefln("Syntax: cmm <mappingModeId>");
        }
    }

    private void cmdSetFunc(char[][] args) {
        if (args.length == 1 && isNumeric(args[0])) {
            try {
                A2Function funcId = cast(A2Function)atoi(args[0]);
                mRendA2.func = funcId;
            } catch (Exception e) {
                writefln("Error: Invalid function id");
            }
        } else {
            writefln("Syntax: setfunc <functionId>");
        }
    }

    private void cmdPositive(char[][] args) {
        if (args.length == 1 && isNumeric(args[0])) {
            int enable = atoi(args[0]);
            mRendA2.doTresholdFunc = cast(bool)enable;
        } else {
            writefln("Syntax: positive 0|1");
        }
    }

    private void cmdSetRend(char[][] args) {
        if (args.length == 1) {
            if (args[0] == "func") {
                mRenderCanvas.renderer = mRendA2;
            } else if (args[0] == "ray") {
                mRenderCanvas.renderer = mRendRay;
            } else if (args[0] == "image") {
                mRenderCanvas.renderer = mImageRenderer;
            } else {
                writefln("Invalid renderer name");
            }
        } else {
            writefln("Syntax: setrend <rendererName>");
        }
    }

    private void cmdLSet(char[][] args) {
        if (args.length == 4) {
            LitShaderLighting litSh = cast(LitShaderLighting)mLitShaders["light"];
            try {
                int idx = atoi(args[0]);
                if (idx >= litSh.lights.count)
                    throw new Exception("");
                litSh.lights.lights[idx].col = RGBColor(
                    atof(args[1]),atof(args[2]),atof(args[3]));
            } catch (Exception e) {
                writefln("Failed to set light color. Invalid id?");
            }
        } else {
            writefln("Syntax: lset <lightIdx> <r> <g> <b>");
        }
    }

    private void cmdLoadLit(char[][] args) {
        if (args.length == 1) {
            try {
                File litFile = new File(args[0]);
                LightSources ls = new LightSources(litFile);
                litFile.close();

                auto litSh = cast(LitShaderLighting)mLitShaders["light"];
                litSh.lights = ls;

                writefln("Loaded %d lights from %s",ls.count,args[0]);
                mLastLightLoad = "loadlit " ~ args[0];
            } catch (Exception e) {
                writefln("Failed to load lit file: %s",e.msg);
            }
        } else {
            writefln("Syntax: loadlit <litFile>");
        }
    }

    private void cmdMatTex(Box[] args) {
        int idx = unbox!(int)(args[0]);
        char[] filename = unbox!(char[])(args[1]);
        try {
            auto c = new FloatCanvas();
            c.loadImageFile(filename);
            auto tex = new BitmapTexture(c);
            if (checkCurrentMat()) {
                mMaterials[mCurrentMat].setTexture(idx,tex);
            }
            mLastTextureLoad = "loadtex " ~ std.string.toString(idx) ~ " " ~ filename;
        } catch (Exception e) {
            writefln("Failed to load image file: %s",e.msg);
        }
    }

    private void cmdMatColor(Box[] args) {
        int idx = unbox!(int)(args[0]);
        RGBColor col = unbox!(RGBColor)(args[1]);
        auto tex = new Texture(col);
        if (checkCurrentMat()) {
            mMaterials[mCurrentMat].setTexture(idx,tex);
        }
    }

    private void cmdAssignMat(Box[] args) {
        char[] matId = unbox!(char[])(args[0]);
        int objMatIdx = -1;
        if (args.length > 1)
            objMatIdx = unbox!(int)(args[1]);
        if (matId in mMaterials) {
            if (objMatIdx >= 0) {
                if (checkCurrentObj()) {
                    mObjects[mCurrentObj].setMaterial(mMaterials[matId],objMatIdx);
                }
            } else {
                mScene.globalMaterial = mMaterials[matId];
            }
        } else {
            writefln("Error: Undefined material \"%s\"",matId);
        }
    }

    private void cmdLoadEnvMap(char[][] args) {
        if (args.length == 1) {
            try {
                if (args[0] == "none") {
                    mEnvironment.texture = null;
                } else {
                    auto c = new FloatCanvas();
                    c.loadImageFile(args[0]);
                    mEnvironment.texture = c;
                    (cast(LitShaderSphHarmonics)mLitShaders["sphharm"]).updateEnv();
                    mLastEnvMapLoad = "loadenvmap " ~ args[0];
                }
                //mLastTextureLoad = "loadtex " ~ args[0];
            } catch (Exception e) {
                writefln("Failed to load image file: %s",e.msg);
            }
        } else {
            writefln("Syntax: loadenvmap <imageFile>");
        }
    }

    private void cmdBumpMap(char[][] args) {
        FloatCanvas bumpTex = new FloatCanvas(Vector2i(400,400));
        genTexNoise(bumpTex,30);
        mRendRay.bumpMap = new BitmapTexture(bumpTex);
    }

    private void cmdMatGenTex(Box[] args) {
        int idx = unbox!(int)(args[0]);
        int width = unbox!(int)(args[1]);
        int height = unbox!(int)(args[2]);
        char[] texName = unbox!(char[])(args[3]);
        char[][] texArgs;
        if (args.length > 4)
            texArgs = unbox!(char[][])(args[4]);
        try {
            auto texSh = new BitmapTexture();
            FloatCanvas tex = new FloatCanvas(Vector2i(width,height));
            char[] cmdGen = "gentex " ~ std.string.toString(width) ~ " " ~ std.string.toString(height) ~ " " ~ texName;
            switch (texName) {
                case "checker":
                    if (texArgs.length >= 1 && isNumeric(texArgs[0])) {
                        genTexChecker(tex,atoi(texArgs[0]));
                        cmdGen ~= " " ~ texArgs[0];
                    } else {
                        tex = null;
                        writefln("  checker: Expecting 1 argument <gridSize>");
                    }
                    break;
                case "noise":
                    if (texArgs.length >= 1 && isNumeric(texArgs[0])) {
                        genTexNoise(tex,atoi(texArgs[0]));
                        cmdGen ~= " " ~ texArgs[0];
                    } else {
                        tex = null;
                        writefln("  noise: Expecting 1 argument <level>");
                    }
                    break;
                default:
                    tex = null;
                    writefln("Invalid texture name");
                    break;
            }
            if (tex) {
                mLastTextureLoad = cmdGen;
                writefln("Texture \"%s\" generated",texName);
                texSh.texture = tex;
                if (checkCurrentMat()) {
                    mMaterials[mCurrentMat].setTexture(idx,texSh);
                }
            }
        } catch (Exception e) {
            writefln("Unknown error generating texture: %s",e.msg);
        }
    }

    private void cmdMatGenVTex(Box[] args) {
        int idx = unbox!(int)(args[0]);
        int width = unbox!(int)(args[1]);
        int height = unbox!(int)(args[2]);
        int depth = unbox!(int)(args[3]);
        char[] texName = unbox!(char[])(args[4]);
        char[][] texArgs;
        if (args.length > 5)
            texArgs = unbox!(char[][])(args[5]);
        try {
            VolTexture texSh = new VolTexture();
            auto tex = new FloatCanvas3D(Vector3i(width,height,depth));
            char[] cmdGen = "genvtex " ~ std.string.toString(width) ~ " " ~
                std.string.toString(height) ~ " " ~ std.string.toString(depth)
                ~ " " ~ texName;
            switch (texName) {
                case "wood":
                    if (texArgs.length >= 1 && isNumeric(texArgs[0])) {
                        genTexWood(tex,atoi(texArgs[0]));
                        cmdGen ~= " " ~ texArgs[0];
                    } else {
                        tex = null;
                        writefln("  wood: Expecting 1 argument <level>");
                    }
                    break;
                case "marble":
                    if (texArgs.length >= 1 && isNumeric(texArgs[0])) {
                        genTexMarble(tex,atoi(texArgs[0]));
                        cmdGen ~= " " ~ texArgs[0];
                    } else {
                        tex = null;
                        writefln("  marble: Expecting 1 argument <level>");
                    }
                    break;
                default:
                    tex = null;
                    writefln("Invalid texture name");
                    break;
            }
            if (tex) {
                mLastVolTextureLoad = cmdGen;
                writefln("Volume texture \"%s\" generated",texName);
                texSh.texture3d = tex;
                if (checkCurrentMat()) {
                    mMaterials[mCurrentMat].setTexture(idx,texSh);
                }
            }
        } catch (Exception e) {
            writefln("Unknown error generating texture: %s",e.msg);
        }
    }

    private void cmdObjTri(Box[] args) {
        char[] objId = unbox!(char[])(args[0]).dup;
        char[] filename = unbox!(char[])(args[1]);
        if (loadTriMesh(objId,filename)) {
            setCurrentObj(objId);
        }
    }

    private bool loadTriMesh(char[] objId, char[] filename) {
        try {
            File triFile = new File(filename);
            TriangleMesh triMesh = new TriangleMesh(triFile);
            triFile.close();
            char[] fnNorms = filename ~ ".norms";
            triMesh.readNormalFile(fnNorms);
            char[] fnMats = filename ~ ".mat";
            triMesh.readMaterialFile(fnMats);
            writefln("Loaded %d raw triangles from %s into \"%s\"",triMesh.getTriangles().length,filename,objId);
            mObjects[objId] = triMesh;
            return true;
        } catch (Exception e) {
            writefln("Failed to load triangle file: %s",e.msg);
            return false;
        }
    }

    private void cmdCurObj(Box[] args) {
        char[] objId = unbox!(char[])(args[0]);
        setCurrentObj(objId);
    }

    private void setCurrentObj(char[] objId) {
        if (objId in mObjects) {
            mCurrentObj = objId;
            writefln("Current object is \"%s\"",objId);
        } else {
            writefln("Error: No such object: %s",objId);
        }
    }

    private void cmdClear(Box[] args) {
        mScene.clear();
    }

    private void addObject(char [] objId) {
        ObjectInstance inst = new ObjectInstance(mObjects[objId],mScale,mRotate,mTranslate);
        mScene.addObject(inst);
    }

    private void cmdInsert(Box[] args) {
        char[] objId = unbox!(char[])(args[0]);
        if (objId in mObjects) {
            addObject(objId);
            resetCam();
        } else {
            writefln("Error: No such object: %s",objId);
        }
    }

    private void resetCam() {
        float e = mScene.maxExtent();
        mCamera.lookAtFrom(e*Vector3f(0, 0, 1.0f) + mScene.middle, mScene.middle);
    }

    private void cmdResetCam(Box[] args) {
        resetCam();
    }

    private void cmdLoadTri(Box[] args) {
        char[] filename = unbox!(char[])(args[0]);
        if (loadTriMesh("default",filename)) {
            mScene.clear();
            setCurrentObj("default");
            addObject("default");
            resetCam();
            mLastSceneLoad = "loadtri " ~ filename;
        }
    }

    private void cmdObjBez(Box[] args) {
        char[] objId = unbox!(char[])(args[0]).dup;
        char[] filename = unbox!(char[])(args[1]);
        if (loadBeziers(objId,filename)) {
            setCurrentObj(objId);
        }
    }

    private bool loadBeziers(char[] objId, char[] filename) {
        try {
            File bezFile = new File(filename);
            Bezier4x4Mesh bezMesh = new Bezier4x4Mesh(bezFile);
            bezFile.close();
            writefln("Loaded %d bezier patches from %s into \"%s\"",bezMesh.patchCount(),filename,objId);
            mObjects[objId] = bezMesh;
            return true;
        } catch (Exception e) {
            writefln("Failed to load bezier file: %s",e.msg);
            return false;
        }
    }

    private void cmdLoadBez(Box[] args) {
        char[] filename = unbox!(char[])(args[0]);
        if (loadBeziers("default",filename)) {
            mScene.clear();
            setCurrentObj("default");
            addObject("default");
            resetCam();
            mLastSceneLoad = "loadbez " ~ filename;
        }
    }

    private bool checkCurrentObj() {
        if (!(mCurrentObj in mObjects)) {
            writefln("Error: Current object \"%s\" invalid",mCurrentObj);
            return false;
        }
        return true;
    }

    private void cmdNewMat(Box[] args) {
        char[] matType = unbox!(char[])(args[0]);
        char[] matId = unbox!(char[])(args[1]).dup;
        switch (matType) {
            case "simple":
                SimpleMaterial simpleMat = new SimpleMaterial();
                mMaterials[matId] = simpleMat;
                writefln("New simple material created named \"%s\"",matId);
                setCurrentMat(matId);
                break;
            case "ashi":
                AshiMaterial ashiMat = new AshiMaterial();
                mMaterials[matId] = ashiMat;
                writefln("New Ashikhmin/Shirley material created named \"%s\"",matId);
                setCurrentMat(matId);
                break;
            default:
                writefln("Available material types:");
                writefln("  simple: Texture/color material");
                writefln("  ashi:   Ashikhmin/Shirley BRDF");
        }
    }

    private void cmdCurMat(Box[] args) {
        char[] matId = unbox!(char[])(args[1]);
        setCurrentMat(matId);
    }

    private bool checkCurrentMat() {
        if (!(mCurrentMat in mMaterials)) {
            writefln("Error: Current material \"%s\" invalid",mCurrentMat);
            return false;
        }
        return true;
    }

    private void setCurrentMat(char[] matId) {
        if (matId in mMaterials) {
            mCurrentMat = matId;
            writefln("Current material is \"%s\"",matId);
        } else {
            writefln("Error: No such material: %s",matId);
        }
    }

    private void cmdSubdiv(char[][] args) {
        if (args.length < 1 || !isNumeric(args[0])) {
            writefln("Syntax: subdiv <count>");
            return;
        }

        mSubdivCount = atoi(args[0]);
        if (checkCurrentObj()) {
            Bezier4x4Mesh bezMesh = cast(Bezier4x4Mesh)mObjects[mCurrentObj];
            if (bezMesh) {
                bezMesh.numSubdivs = atoi(args[0]);
            } else {
                writefln("Error: Current object is not a bezier mesh");
            }
        }
    }

    private void cmdNormIp(char[][] args) {
        if (args.length < 1 || !isNumeric(args[0])) {
            writefln("Syntax: normip 0|1");
            return;
        }

        mScene.normInterpolate = cast(bool)atoi(args[0]);
    }

    private void cmdSetCam(char[][] args) {
        if (args.length != 9) {
            writefln("Syntax: setcam <x> <y> <z> <dx> <dy> <dz> <ux> <uy> <uz>");
            return;
        }

        try {
            Vector3f p = Vector3f(atof(args[0]),atof(args[1]),atof(args[2]));
            Vector3f d = Vector3f(atof(args[3]),atof(args[4]),atof(args[5]));
            Vector3f u = Vector3f(atof(args[6]),atof(args[7]),atof(args[8]));

            mCamera.set(p,d,u);
        } catch (Exception e) {
            writefln("Error setting cam position: %s",e.msg);
        }
    }

    private void cmdSaveView(char[][] args) {
        if (args.length < 1) {
            writefln("Syntax: saveview <filename>");
            return;
        }

        File scrFile = new File(args[0],FileMode.Out);
        scope(exit)scrFile.close();
        if (mRenderCanvas.renderer == mRendRay) {
            scrFile.writefln("subdiv %d",mSubdivCount);
            scrFile.writefln(mLastSceneLoad);
            if (mLastLightLoad.length > 0)
                scrFile.writefln(mLastLightLoad);
            if (mLastTextureLoad.length > 0)
                scrFile.writefln(mLastTextureLoad);
            if (mEnvironment.texture)
                scrFile.writefln(mLastEnvMapLoad);
            scrFile.writefln("setcam %a %a %a %a %a %a %a %a %a",
                mCamera.position.x,mCamera.position.y,mCamera.position.z,
                mCamera.direction.x,mCamera.direction.y,mCamera.direction.z,
                mCamera.up.x,mCamera.up.y,mCamera.up.z);
            scrFile.writefln("setcamlight %s",mRendRay.cameralight?"on":"off");

            char[] litShKey = "none";
            foreach (char[] key, AbstractShader sh; mLitShaders) {
                if (sh is mRendRay.lightingShader) {
                    litShKey = key;
                    break;
                }
            }
            scrFile.writefln("shlit %s",litShKey);
            scrFile.writefln("normip %d",cast(int)mScene.normInterpolate);
            scrFile.writefln("setrend ray");
            scrFile.writefln("setrecdepth %d",mRendRay.recursiondepth);
        } else if (mRenderCanvas.renderer == mRendA2) {
            scrFile.writefln("setfunc %d",cast(int)mRendA2.func);
            scrFile.writefln("positive %d",mRendA2.doTresholdFunc);
            scrFile.writefln("setrend func");
        } else if (mRenderCanvas.renderer == mImageRenderer) {
            scrFile.writefln("%s",mLastImageLoad);
            scrFile.writefln("setrend image");
        }
        scrFile.writefln("setsize %d %d",mRes.x,mRes.y);
        if (mRenderCanvas.antialias)
            scrFile.writefln("aa %d",cast(int)mAntialias.antialiasMethod);
        else
            scrFile.writefln("aa none");
        scrFile.writefln("aas lin %d",mAntialias.linearSampleCount);
        scrFile.writefln("aas sq %d",mAntialias.squareSampleCount);
        scrFile.writefln("cmm %d",cast(int)mRenderCanvas.colorMappingMode);
    }

    private void cmdSavePNG(char[][] args) {

        bool progress(int curLine, int numLines, int pixelRes) {
            writef("Rendering...%d%%\r",(100*curLine)/numLines);
            fflush(stdout);
            return true;
        }

        void renderAndSave(Vector2i size, char[] filename) {
            FloatCanvas fcanOut = new FloatCanvas(size);
            mRenderCanvas.render(fcanOut);
            progress(1,1,0);
            writefln();
            fcanOut.savePNG(filename);
            delete fcanOut;
        }

        bool delegate(int curLine, int numLines, int pixelRes) oldProgress;
        oldProgress = mRenderCanvas.onProgress;
        mRenderCanvas.onProgress = &progress;
        try {
            if (args.length == 3) {
                Vector2i size = Vector2i(atoi(args[0]),atoi(args[1]));
                renderAndSave(size,args[2]);
            } else if (args.length == 1) {
                renderAndSave(mRes,args[0]);
            } else {
                writefln("Syntax: savepng <width> <height> <filename>");
                writefln("    Or: savepng <filename>");
            }
        } catch (Exception e) {
            writefln("Failed to save PNG: %s", e.msg);
        }
        mRenderCanvas.onProgress = oldProgress;
    }

    private void cmdLoadImage(char[][] args) {
        if (args.length != 1) {
            writefln("Syntax: loadimg <filename>");
            return;
        }

        try {
            FloatCanvas canvas = new FloatCanvas();
            canvas.loadImageFile(args[0]);
            mImageRenderer.image = canvas;
            mRenderCanvas.renderer = mImageRenderer;
            mLastImageLoad = "loadimg " ~ args[0];
        } catch (Exception e) {
            writefln("Failed to load image %s: %s", args[0], e.msg);
        }
    }

    private void cmdSetSize(char[][] args) {
        if (args.length < 1) {
            writefln("Syntax: setsize <width> <height>");
            writefln("    Or: setsize img");
            return;
        }

        Vector2i size;
        if (args[0] == "img") {
            //resize output to fit image size
            if (!mImageRenderer.image) {
                writefln("Error: No image loaded");
                return;
            }
            size = mImageRenderer.image.size();
        } else if (args[0] == "img2") {
            //resize output to half of the image size
            if (!mImageRenderer.image) {
                writefln("Error: No image loaded");
                return;
            }
            size = mImageRenderer.image.size()/2;
        } else if (isNumeric(args[0]) && isNumeric(args[1]))
            //resize to given width/height
            size = Vector2i(atoi(args[0]),atoi(args[1]));
        else {
            writefln("Error: Invalid parameter");
            return;
        }
        setSize(size);
    }

    private void cmdSimpleReflect(Box[] args) {
        mRendRay.simpleReflect = unbox!(bool)(args[0]);
    }

    private void cmdTexFilt(Box[] args) {
        int idx = unbox!(int)(args[0]);
        TextureFilter filtId = cast(TextureFilter)unbox!(int)(args[1]);
        if (checkCurrentMat()) {
            Texture tex = mMaterials[mCurrentMat].getTexture(idx);
            if (tex) {
                BitmapTexture bmTex = cast(BitmapTexture)tex;
                if (bmTex)
                    bmTex.filter = filtId;
                else
                    writefln("Error: Texture %d is not a bitmap texture",idx);
            } else {
                writefln("Error: Invalid texture index %d",idx);
            }
        }
    }

    private void cmdSunAnim(Box[] args) {
        bool state = unbox!(bool)(args[0]);
        mSunAnim = state;
    }

    private void cmdSetTime(Box[] args) {
        float hour = unbox!(float)(args[0]);
        mSunTime = hour;
        (cast(LitShaderSun)mLitShaders["sun"]).dirByTime(mSunDate,mSunTime);
    }

    private void cmdExec(char[][] args) {
        if (args.length < 1) {
            writefln("Syntax: exec <scriptFile>");
            return;
        }

        try {
            File scrFile = new File(args[0], FileMode.In);
            scope(exit) scrFile.close();
            writefln("Executing %s",args[0]);
            mScene.beginUpdate();
            mCmdProc.executeScript(scrFile);
            mScene.endUpdate();
        } catch (Exception e) {
            writefln("Failed to execute script %s: %s", args[0],e.msg);
        }

    }

    private void cmdBind(char[][] args) {
        if (args.length < 2) {
            writefln("Syntax: bind <modifier>* <keyName> <actionId>");
            return;
        }

        //check modifiers
        int mods = 0;
        for (int i = 0; i < args.length-2; i++) {
            switch (args[i]) {
                case "alt":
                    mods |= KMOD_LALT;
                    break;
                case "ctrl":
                    mods |= KMOD_LCTRL;
                    break;
                case "shift":
                    mods |= KMOD_LSHIFT;
                default:
                    writefln("ERROR: unknown modifier >%s<", args[i]);
            }
        }

        Keycode code = translateKeyIDToKeycode(args[$-2]);
        if (code == Keycode.INVALID) {
            writefln("Invalid keyname: %s",args[$-2]);
            return;
        }
        int sdlkey = translateKeycodeToSDLKey(code);

        BindItem bind;
        bind.sym = sdlkey;
        bind.mod = mods;

        //only crap without the .dup (??)
        mKeyMap[bind] = args[$-1].dup;
        writefln("Bind: %s = %s, mod=%s", args[$-1], args[$-2], mods);
    }

    private void cmdUnBind(char[][] args) {
        //obsolete with new mKeyMap structure
        //xxx do we need unbind on keys, e.g. unbind alt left?

        /*if (args.length != 1 || !(args[0] in mKeyMap)) {
            writefln("Syntax: unbind <action>");
            return;
        }
        mKeyMap.remove(args[0]);*/
    }

    private void cmdUnbindAll(char[][] args) {
        if (args.length > 0) {
            writefln("Syntax: unbindall");
            return;
        }

        mKeyMap = null;
        writefln("All key bindings removed");
    }

    private void cmdChDir(char[][] args) {
        if (args.length < 1) {
            writefln("Syntax: chdir <newDir>");
            return;
        }

        chdir(args[0]);
    }

    private void updateFPS(float curFPS) {
        char[] title = std.string.toString(curFPS) ~ " fps";
        mScreenCanvas.setWindowTitle(title);
        mConsole.checkCommands();
        tick();
    }

    private bool renderProgress(int curLine, int numLines, int pixelRes) {
        if (pixelRes <= 1 || mAllowLowResEvents)
            mScreenCanvas.processEvents();
        if (mAbortCurrentFrame && pixelRes < mLowPixelRes) {
            //abort when cam has been moved while rendering hi-res
            mAbortCurrentFrame = false;
            return false;
        }
        return !(mScreenCanvas.terminated | mConsole.commandsPending());
    }
}


int main(char[][] args)
{
    Application app = new Application(args);
    return app.run();
    //hallo
}
